﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.Reflection;
using System.Xml.Linq;

namespace Ghost.Tasks.Wiki.XApi
{
    public abstract partial class XType
    {
        [XmlIgnore]
        public Type type;
        [XmlIgnore]
        public Type explicitInterfaceType;
        [XmlIgnore]
        public XAssembly assembly;
        [XmlIgnore]
        public List<XTypeMember> members = new List<XTypeMember>();
        public abstract void ApplyMembers();
    }

    public class GenericArgumentPlaceholderHost<__T0, __T1, __T2, __T3, __T4, __T5, __T6, __T7, __T8, __T9, __T10, __T11, __T12, __T13>
    {
    }

    public static class XTypeExtensions
    {

        public static IEnumerable<XField> BuildXFields(this XType output, Type type, XElement typeElement)
        {
            List<XField> xFields = new List<XField>();
            foreach (FieldInfo fieldInfo in type.GetFields(BindingFlags.Public | BindingFlags.Static))
            {
                if (!fieldInfo.IsPublic)
                    continue;
                string xid = fieldInfo.ToXmlCommentID();
                XElement commentElement = output.assembly.GetCommentElement(xid);
                XField xField = output.BuildXField(fieldInfo, commentElement, xid);
                output.assembly.project.AddMember(xField);
                xFields.Add(xField);
            }
            return xFields;
        }

        public static XField BuildXField(this XType xType, FieldInfo fieldInfo, XElement fieldMemberElement, string id)
        {
            XField output = new XField();
            output.id = id;
            output.caption = output.memberName = fieldInfo.Name;
            output.anchor = string.Concat("#", fieldInfo.Name);
            output.wikiLink = string.Concat(xType.wikiLink, output.anchor);
            output.BuildComments(fieldMemberElement);
            output.isPublic = fieldInfo.IsPublic;
            output.isStatic = fieldInfo.IsStatic;
            output.isLiteral = fieldInfo.IsLiteral;
            if (fieldInfo.IsLiteral)
            {
                output.literalValue = fieldInfo.GetRawConstantValue().ToString();
            }
            output.memberInfo = fieldInfo;
            output.valueType = fieldInfo.FieldType.ToXMemberRef();
            //BuildAttributesElement(memberElement, memberInfo.GetCustomAttributes(false));
            return output;
        }


        public static IEnumerable<XMethod> BuildXMethods(this XType output, Type type, XElement typeElement)
        {
            List<XMethod> xMethods = new List<XMethod>();
            foreach (MethodInfo methodInfo in type.GetMethods(BindingFlags.Public | BindingFlags.Static))
            {
                if (!methodInfo.IsPublic)
                    continue;
                string xid = methodInfo.ToXmlCommentID();
                XElement commentElement = output.assembly.GetCommentElement(xid);
                XMethod xMethod = output.BuildXMethod(methodInfo, commentElement, xid);
                output.assembly.project.AddMember(xMethod);
                xMethods.Add(xMethod);
            }
            return xMethods;
        }

        public static XMethod BuildXMethod(this XType xType, MethodInfo methodInfo, XElement methodMemberElement, string id)
        {
            XMethod output = new XMethod();
            output.id = id;
            output.caption = output.memberName = methodInfo.Name;
            output.anchor = string.Concat("#", methodInfo.Name);
            output.wikiLink = string.Concat(xType.wikiLink, output.anchor);
            output.BuildComments(methodMemberElement);
            output.isPublic = methodInfo.IsPublic;
            output.isStatic = methodInfo.IsStatic;
            
            output.memberInfo = methodInfo;
            //BuildAttributesElement(memberElement, memberInfo.GetCustomAttributes(false));
            return output;
        }


        public static IEnumerable<XEvent> BuildXEvents(this XType output, Type type, XElement typeElement)
        {
            List<XEvent> xEvents = new List<XEvent>();
            foreach (EventInfo eventInfo in type.GetEvents(BindingFlags.Public | BindingFlags.Static))
            {
                string xid = eventInfo.ToXmlCommentID();
                XElement commentElement = output.assembly.GetCommentElement(xid);
                XEvent xEvent = output.BuildXEvent(eventInfo, commentElement, xid);
                output.assembly.project.AddMember(xEvent);
                xEvents.Add(xEvent);
            }
            return xEvents;
        }

        public static XEvent BuildXEvent(this XType xType, EventInfo eventInfo, XElement eventMemberElement, string id)
        {
            XEvent output = new XEvent();
            output.id = id;
            output.caption = output.memberName = eventInfo.Name;
            output.anchor = string.Concat("#", eventInfo.Name);
            output.wikiLink = string.Concat(xType.wikiLink, output.anchor);
            output.BuildComments(eventMemberElement);
            output.memberInfo = eventInfo;
            //BuildAttributesElement(memberElement, memberInfo.GetCustomAttributes(false));
            return output;
        }

        public static IEnumerable<XProperty> BuildXProperties(this XType output, Type type, XElement typeElement)
        {
            List<XProperty> xProperties = new List<XProperty>();
            foreach (PropertyInfo propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Static))
            {
                string xid = propertyInfo.ToXmlCommentID();
                XElement commentElement = output.assembly.GetCommentElement(xid);
                XProperty xProperty = output.BuildXProperty(propertyInfo, commentElement, xid);
                output.assembly.project.AddMember(xProperty);
                xProperties.Add(xProperty);
            }
            return xProperties;
        }

        public static XProperty BuildXProperty(this XType xType, PropertyInfo propInfo, XElement fieldMemberElement, string id)
        {
            XProperty output = new XProperty();
            output.id = id;
            output.caption = output.memberName = propInfo.Name;
            output.anchor = string.Concat("#", propInfo.Name);
            output.wikiLink = string.Concat(xType.wikiLink, output.anchor);
            output.BuildComments(fieldMemberElement);
            output.memberInfo = propInfo;
            output.valueType = propInfo.PropertyType.ToXMemberRef();
            output.memberInfo = propInfo;
            output.canRead = propInfo.CanRead;
            output.canWrite = propInfo.CanWrite;
            output.get = propInfo.GetGetMethod(false).ToXMemberRef();
            output.set = propInfo.GetSetMethod(false).ToXMemberRef();
            //BuildAttributesElement(memberElement, memberInfo.GetCustomAttributes(false));
            return output;
        }


        private static void BuildProperty(this XType type, XProperty output, XElement typeMemberElement)
        {
            PropertyInfo memberInfo;
            try
            {
                memberInfo = type.GetProperty(output);
            }
            catch (AmbiguousMatchException)
            {
                type.LogWarningFindFailed(() => type.type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Static), "Found multiple properties matching \"{0}\"", output);
                return;
            }
            if (memberInfo == null)
            {
                type.LogWarningFindFailed(() => type.type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Static), "Found no property matching \"{0}\"", output);
                return;
            }
            output.memberInfo = memberInfo;
            output.canRead = memberInfo.CanRead;
            output.canWrite = memberInfo.CanWrite;
            output.get = memberInfo.GetGetMethod(false).ToXMemberRef();
            output.set = memberInfo.GetSetMethod(false).ToXMemberRef();
            //BuildParametersElement(memberElement, memberInfo.GetIndexParameters());
            //BuildMemberRef(memberElement, memberInfo.PropertyType);
            //BuildAttributesElement(memberElement, memberInfo.GetCustomAttributes(false));
        }


        public static void BuildTypeMember(this XType type, XElement typeMemberElement, string id)
        {
           
            //Detect explicit interface implementation
            //if (id.Contains('#') && !(id.Contains(".#")))
            //{
            //    int lastDot = id.LastIndexOf('.');
            //    string explicitMemberName = id.Substring(lastDot + 1).Replace('#', '.').Replace('@', ',');
            //    int firstParen = explicitMemberName.IndexOf('(');
            //    if (firstParen >= 0)
            //    {
            //        lastDot = explicitMemberName.LastIndexOf('.', 0, firstParen);
            //    }
            //    else
            //    {
            //        lastDot = explicitMemberName.LastIndexOf('.');
            //    }
            //    output.memberName = explicitMemberName.Substring(lastDot + 1);
            //    output.explicitInterface = explicitMemberName.Substring(0, lastDot);
            //    type.explicitInterfaceType = type.assembly.project.GetType(output.explicitInterface);
            //    if (type.explicitInterfaceType == null)
            //    {
            //        type.LogWarning("Could not find interface used in explicit instantiation \"{0}\"", output.explicitInterface);
            //        return;
            //    }
            //}

            //output.id = id;
            //output.caption = output.memberName = output.memberName ?? id.Substring(type.id.Length + 1);
            //output.anchor = string.Concat("#", output.memberName);
            //output.wikiLink = string.Concat(type.wikiLink, output.anchor);
            //output.BuildComments(typeMemberElement);
            //type.assembly.project.AddMember(output);
            //switch (id[0])
            //{
               
            //        break;
            //    case 'E':
            //        BuildEvent(type, output as XEvent, typeMemberElement);
            //        break;
            //    case 'M':
            //        BuildMethod(type, output as XMethod, typeMemberElement);
            //        break;
            //    case 'P':
            //        BuildProperty(type, output as XProperty, typeMemberElement);
            //        break;
            //}
            //type.members.Add(output);
        }

        

        private static void BuildMethod(this XType type, XMethod output, XElement typeMemberElement)
        {
            string rawMemberName = output.memberName;
            string rawMethodName = rawMemberName;

            List<Type> paramTypes = null;
            List<ParameterModifier> paramModifiers = null;

            MemberInfo memberInfo = null;
            ConstructorInfo constInfo;
            MethodInfo methodInfo;

            //See if this is a previously identified extension method
            output.isExtension = type.assembly.TryGetExtensionMethod(output.id, out methodInfo);

            if (output.isExtension)
            {
                //We have already resolved the method info
                memberInfo = methodInfo;
                rawMethodName = methodInfo.Name;
            }
            else
            {
                //Check for parameters, we may need them to resolve the method
                int firstParen = rawMemberName.IndexOf('(');
                if (firstParen >= 0)
                {
                    paramTypes = new List<Type>();
                    paramModifiers = new List<ParameterModifier>();

                    //Pull out the method name
                    rawMethodName = rawMemberName.Substring(0, firstParen);
                    //Split out the parameters
                    string parameters = rawMemberName.Substring(firstParen + 1);
                    parameters = parameters.Remove(parameters.Length - 1);
                    ParameterModifier paramModifier;

                    output.isGenericMethodDefinition = rawMethodName.Contains("``");

                    type.ParseMethodParameters(parameters, out paramTypes, out paramModifier);
                    if (paramTypes != null)
                    {

                        paramModifiers.Add(paramModifier);
                    }
                }
                else
                {
                    rawMethodName = rawMemberName;
                    output.isGenericMethodDefinition = rawMethodName.Contains("``");
                }
            }

            //Default attribute resolution (in case we can't do better later)
            output.anchor = string.Concat("#", rawMethodName);
            output.wikiLink = string.Concat(type.wikiLink, output.anchor);
            output.caption = rawMethodName;
            output.name = rawMemberName;

            if (memberInfo == null)
            {
                if (rawMethodName.StartsWith("#ctor"))
                {
                    //Resolve a constructor by name, parameters, and modifiers
                    rawMethodName = type.type.Name;

                    memberInfo = constInfo = type.GetConstructor(paramTypes, paramModifiers);

                    if (constInfo == null)
                    {
                        type.LogWarningFindFailed(() => type.GetConstructors(), "Found exact constructor match for {0}", output);
                        return;
                    }
                    output.isPublic = constInfo.IsPublic;
                    output.isConstructor = true;
                    output.isConstructorSpecified = true;
                    output.name = type.name;
                    output.caption = memberInfo.ToString().Replace("Void .ctor", type.name).ToCaption();
                }
                else
                {
                    //Resolve a member by name, parameters, and modifiers
                    memberInfo = methodInfo = type.GetMethod(rawMethodName, paramTypes, paramModifiers);
                    if (methodInfo == null)
                    {
                        type.LogWarningFindFailed(() => type.GetMethods(), "Found no exact method match for {0}", output);
                        return;
                    }
                    output.isPublic = methodInfo.IsPublic;
                    output.name = methodInfo.Name;
                    output.caption = methodInfo.ToCaption();
                }
            }

            output.anchor = string.Concat("#", output.caption.ToAnchor());
            output.wikiLink = string.Concat(type.wikiLink, output.anchor);

            output.memberInfo = memberInfo;
        }

        private static void BuildEvent(this XType type, XEvent output, XElement typeMemberElement)
        {
            //EventInfo memberInfo = GetMemberInfo<EventInfo>(lastType, rawMemberName, MemberTypes.Event);
            //if (memberInfo == null)
            //    return false;
            //output.memberInfo = memberInfo;
            //output.isMulticast= memberInfo.IsMulticast;
            //BuildMethodElement(memberElement, "add", memberInfo.GetAddMethod(false),0);
            //BuildMethodElement(memberElement, "remove", memberInfo.GetRemoveMethod(false),0);
            //BuildMemberRef(memberElement, memberInfo.EventHandlerType);
            //BuildAttributesElement(memberElement, memberInfo.GetCustomAttributes(false));         
        }

        public static FieldInfo[] GetFields(this XType xType)
        {
            return xType.type.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
        }

        public static FieldInfo GetField(this XType xType, string fieldName)
        {
            return GetFields(xType).FirstOrDefault(fi => fi.Name == fieldName);
        }


        public static MethodInfo[] GetMethods(this XType xType)
        {
            return xType.type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly);
        }

        public static MethodInfo GetMethod(this XType xType, string rawMethodName, List<Type> paramTypes, List<ParameterModifier> paramModifiers)
        {
            Type type = xType.type;
            int firstTic = rawMethodName.IndexOf('`');
            bool gueryingForGeneric = firstTic >= 0;
            if (gueryingForGeneric)
            {
                string suffix = rawMethodName.Substring(firstTic);
                rawMethodName = rawMethodName.Substring(0, firstTic);
            }

            List<MethodInfo> candidates = new List<MethodInfo>(xType.GetMethods().Where(mi => mi.Name == rawMethodName));
            if (candidates.Count < 2)
                return candidates.FirstOrDefault();

            //Filter by generic
            candidates.RemoveAll(mi => (mi.IsGenericMethod || mi.IsGenericMethodDefinition) != gueryingForGeneric);
            if (candidates.Count < 2)
                return candidates.FirstOrDefault();

            if (paramTypes == null)
            {
                //Handle no parameters case
                paramTypes = new List<Type>();
            }

            //Filter by parameter count
            candidates.RemoveAll(mi =>
            {
                ParameterInfo[] parameters = mi.GetParameters();
                if (parameters.Length != paramTypes.Count)
                    return true; //drop...we don't have the same number of parameter
                return false;
            });
            if (candidates.Count < 2)
                return candidates.FirstOrDefault();

            candidates.RemoveAll(mi =>
            {
                ParameterInfo[] parameters = mi.GetParameters();
                
                return parameters.Zip(paramTypes, (pi, paramType) =>
                {
                    if (pi.ParameterType.IsGenericParameter)
                    {
                        if (!paramType.IsGenericParameterPlaceholder())
                            return true;
                        else if (pi.ParameterType.GenericParameterPosition != paramType.GetGenericParameterPlaceholderPosition())
                            return true;
                    }
                    else if (pi.ParameterType.FullName == null)
                    {
                        if (pi.ParameterType.Name != paramType.Name)
                            return true;
                    }
                    else if ((paramType.FullName ?? paramType.Name) != (pi.ParameterType.FullName ?? pi.ParameterType.Name))
                        return true;
                    return false;
                }).Any(b => b);
            });
            if (candidates.Count == 1)
                return candidates.FirstOrDefault();

            if (!candidates.Any())
            {
                xType.assembly.LogWarning("Eliminated all possible method matches for {0}", rawMethodName);
            }
            else if (candidates.Count > 1)
            {
                xType.assembly.LogWarningFindFailed(()=>candidates.ToList(), "Ambiguous results for method {0}", rawMethodName);
            }

            return null;
        }

        public static ConstructorInfo[] GetConstructors(this XType xType)
        {
            return xType.type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
        }

        public static ConstructorInfo GetConstructor(this XType xType, List<Type> paramTypes, List<ParameterModifier> paramModifiers)
        {
            Type type = xType.type;

            List<ConstructorInfo> candidates = new List<ConstructorInfo>(xType.GetConstructors());
            if (!candidates.Any())
            {
                xType.assembly.LogWarningFindFailed(()=>xType.type.GetConstructors(), "Found no constructors for {0}", xType);
            }

            if (candidates.Count < 2)
                return candidates.FirstOrDefault();

            if (paramTypes == null)
            {
                //consider Default constructors
                paramTypes = new List<Type>();
            }
            
            candidates.RemoveAll(mi =>
            {
                ParameterInfo[] parameters = mi.GetParameters();
                if (parameters.Length != paramTypes.Count)
                    return true; //drop...we don't have the same number of parameters

                return parameters.Zip(paramTypes, (pi, paramType) =>
                {
                    if (pi.ParameterType.IsGenericParameter)
                    {
                        if (!paramType.IsGenericParameterPlaceholder())
                            return true;
                        else if (pi.Position != paramType.GetGenericParameterPlaceholderPosition())
                            return true;
                    }
                    else if ((paramType.FullName ?? paramType.Name) != (pi.ParameterType.FullName ?? pi.ParameterType.Name))
                        return true;
                    return false;
                }).Any(b => b);
            });
            if (candidates.Count < 2)
                return candidates.FirstOrDefault();
            

            return null;
        }



        public static PropertyInfo GetProperty(this XType xType, XProperty output)
        {
            PropertyInfo propertyInfo;
            int parenPos = output.memberName.IndexOf('(');
            if (parenPos < 0)
            {
                propertyInfo = (xType.explicitInterfaceType ?? xType.type).GetProperty(output.memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Static);
            }
            else
            {
                List<ParameterModifier> paramModifiers = null;
                List<Type> paramTypes = new List<Type>();
                paramModifiers = new List<ParameterModifier>();

                //Pull out the prop name
                string rawPropName = output.memberName.Substring(0, parenPos);
                //Split out the parameters
                string parameters = output.memberName.Substring(parenPos + 1);
                parameters = parameters.Remove(parameters.Length - 1);
                ParameterModifier paramModifier;
                xType.ParseMethodParameters(parameters, out paramTypes, out paramModifier);
                if (paramTypes != null)
                {
                    paramModifiers.Add(paramModifier);
                    propertyInfo = (xType.explicitInterfaceType ?? xType.type).GetProperty(rawPropName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Static, null, null, paramTypes.ToArray(), paramModifiers.ToArray());
                }
                else
                {
                    propertyInfo = (xType.explicitInterfaceType ?? xType.type).GetProperty(rawPropName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Static);
                }
            }

            if (xType.explicitInterfaceType != null && propertyInfo != null)
            {
                InterfaceMapping map = xType.type.GetInterfaceMap(xType.explicitInterfaceType);

                int indexGet = Array.IndexOf(map.InterfaceMethods, propertyInfo.GetGetMethod(true));
                int indexSet = Array.IndexOf(map.InterfaceMethods, propertyInfo.GetSetMethod(true));
                 
                MethodInfo get = indexGet >= 0 ? map.TargetMethods[indexGet] : null;
                MethodInfo set = indexSet >= 0 ? map.TargetMethods[indexSet] : null;

                PropertyInfo[] props = xType.type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
                foreach (PropertyInfo prop in props)
                {
                    MethodInfo get2 = prop.GetGetMethod(true);
                    if (get2 != null && (get != null && get2.Name == get.Name))
                        return prop;
                    MethodInfo set2 = prop.GetSetMethod(true);
                    if (set2 != null && (set != null && set2.Name == set.Name))
                        return prop;
                }

            }

            return propertyInfo;
        }

        public static void ParseMethodParameters(this XType xType, string parameters, out List<Type> paramTypes, out ParameterModifier parameterModifier)
        {
            paramTypes = new List<Type>();

            string[] paramParts = parameters.CommaSplit();

            //In case we encounter a pass-by-ref parameter
            parameterModifier = new ParameterModifier(paramParts.Length);

            int iParamIndex = 0;
            //Process each parameter individually
            foreach (string paramPart in paramParts)
            {
                bool isRef = false;
                string safeParamPart = paramPart;
                if (paramPart.EndsWith("@"))
                {
                    safeParamPart = paramPart.Remove(paramPart.Length - 1);
                    isRef = true;
                }

                Type paramType = null;
                if ( safeParamPart.StartsWith("``"))
                {
                    int pos = int.Parse(safeParamPart.Substring(2));
                    paramType = typeof(GenericArgumentPlaceholderHost<,,,,,,,,,,,,,>).GetGenericArguments()[pos];
                }
                else if (safeParamPart.StartsWith("`") && xType.type != null && xType.type.IsGenericTypeDefinition)
                {
                    //a generic parameter defined in the generic type definition
                    paramType = ParseGenericTypeMethodParameter(xType.type, safeParamPart);
                }
                else
                {
                    //a normal parameter, just load the related type
                    paramType = xType.assembly.project.GetType(safeParamPart);
                }

                if (paramType != null)
                {
                    paramTypes.Add(paramType);
                    parameterModifier[iParamIndex] = isRef;
                }
                else
                {
                    xType.assembly.LogWarning("Failed to resolve parameter type for {0}", safeParamPart);
                    //Bail                    
                    paramTypes = null;
                    break;
                }
                iParamIndex++;
            }
        }

        private static Type ParseGenericTypeMethodParameter(Type parentType, string typeName)
        {
            string genericName = typeName;
            bool buildArray = typeName.EndsWith("[]");
            if (buildArray) //Parse array syntax
            {
                genericName = typeName.Remove(typeName.Length - 2);
            }

            int paramIndex = int.Parse(genericName.Substring(1));
            Type typeArg = parentType.GetGenericArguments()[paramIndex];

            if (buildArray)
            {
                typeArg = typeArg.MakeArrayType();
            }

            return typeArg;
        }

        

        public static string MangleGenericTypeNames(string typeName)
        {
            int firstCurly = typeName.IndexOf("{");
            if (firstCurly >= 0)
            {
                string prefix = typeName.Substring(0, firstCurly);
                string suffix = typeName.Substring(firstCurly + 1);
                string array = string.Empty;
                if (typeName.EndsWith("]"))
                {
                    int brackPos = suffix.IndexOf('[');
                    array = suffix.Substring(brackPos, suffix.Length - brackPos);
                    suffix = suffix.Remove(brackPos - 1);
                }
                else
                {
                    suffix = suffix.Remove(suffix.Length - 1);
                }

                string[] parts = suffix.CommaSplit();
                string typeCount = string.Format("`{0}", parts.Length);
                List<string> mangledTypeNames = new List<string>(parts.Length);
                foreach (string part in parts)
                {
                    mangledTypeNames.Add(MangleGenericTypeNames(part));
                }
                return string.Concat(prefix, typeCount, "[", string.Join(",", mangledTypeNames), "]", array);
            }
            else
                return typeName;
        }
    }

}
